package com.webserver.http;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import static com.webserver.http.HttpContext.CR;
import static com.webserver.http.HttpContext.LF;

/**
 * 响应对象
 * 该类的每一个实例用于表示一个HTTP协议规定的响应内容
 * 每个响应由三部分组成
 * 状态行 响应头 响应正文
 */
public class HttpServletResponse {
    //状态行的相关信息
    private int statusCode = 200;
    private String statusReason = "OK";
    private Socket socket;
    //响应头相关信息
    //key:响应头的名字 value:响应头的值
    private Map<String, String> headers = new HashMap<>();
    //响应正文的相关内容
    private File contentFile;
    /*
    * 使用字节数组输出流中的字节数组作为正文内容的载体
    * 是一个低级流,其内部维护了一个字节数组,通过这个流写出的内容都会写在这个字节数组中
    * */
    private ByteArrayOutputStream baos;
    private byte[] contentData;//保存动态数据的(数据是从baos中获取的)
    public HttpServletResponse(Socket socket) {
        this.socket = socket;
    }

    /*
     * 发送响应
     * 将当前响应对象内容按照标准的响应格式发送给客户端
     * HTTP/1.1 200 OK(CRLF) 状态行
     * Content-Type: text/html(CRLF) 响应头
     * Content-Length: 2345(CRLF)(CRLF)
     * 1010101010101010.... 响应正文
     */
    public void response() throws IOException {
        sendBefore();
        //3.1发送状态行
        sendStatusLine();
        //3.2发送响应头
        sendHeaders();
        //3.3发送响应正文
        sendContent();
    }
    /*
    * 响应发送前的准备工作
    * */
    private void sendBefore(){
        if (baos!=null){//如果baos不为空,说明我们用过这个流写出过动态数据
            //获取baos流中的字节数组(存储了响应正文的内容)
            contentData = baos.toByteArray();
            //根据该数组的长度设置响应头Content-Length,值用字节数组的长度即可
            addHeader("Content-Length", contentData.length+"");
        }
    }
    //发送状态行
    private void sendStatusLine() throws IOException {
        String line = "HTTP/1.1" + " " + statusCode + " " + statusReason;
        //协议版本(SP)状态码(SP)状态描述
        println(line);
    }

    //发送响应头
    private void sendHeaders() throws IOException {
        //遍历headers将其中所有的响应头发送给浏览器
        //通过entrySet方法获取Map中的所有键值对,封装到Set集合中
        Set<Map.Entry<String, String>> entrySet = headers.entrySet();
        //遍历Set集合,获取其中每一组键值对
        for (Map.Entry<String, String> e : entrySet) {
            String name = e.getKey();//响应头的名字
            String value = e.getValue();//响应头的值
            //Content-Type: text/html
            //将响应头的名字和响应头的值按照HTTP协议要求的格式组织成字符串
            String line = name + ": " + value;
            //将响应头发送给浏览器
            println(line);
            System.out.println("发送响应头:" + line);
        }
        //单独再发送一个回车+换行,标志响应头的结束
        println("");
    }

    //发送响应正文
    private void sendContent() throws IOException {
        OutputStream out = socket.getOutputStream();
        if (contentData!=null){//有动态数据
            out.write(contentData);
        }else if (contentFile != null) {
            //定义缓存区10K,加速读取效率
            byte[] buf = new byte[10 * 1024];
            //定义长度变量,记录我们读到实际长度
            int len;
            //定义文件字节输入流
            try (//自动执行完代码之后,流资源会自动关闭
                 FileInputStream fis = new FileInputStream(contentFile)
            ) {
                //循环读取指定缓冲区大小的数据,如果读取到内容就返回实际读取长度,读取不到就返回-1
                while ((len = fis.read(buf)) != -1) {
                    out.write(buf, 0, len);
                }
            }
        }
        System.out.println("响应正文发送完毕!!!");
    }

    /*
     * 向客户端发送一行字符串(将字符串转换为字节,通过字节流输出到浏览器)
     */
    private void println(String line) throws IOException {
        //获取一个链接远端计算机的输出流,会将字节发送给连接的浏览器
        OutputStream out = socket.getOutputStream();
        //将字符串转换为字节数组
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        //将字节数组发送给浏览器
        out.write(data);
        //发送回车+换行
        out.write(CR);
        out.write(LF);
    }

    public int getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public String getStatusReason() {
        return statusReason;
    }

    public void setStatusReason(String statusReason) {
        this.statusReason = statusReason;
    }

    public File getContentFile() {
        return contentFile;
    }

    public void setContentFile(File contentFile) {
        this.contentFile = contentFile;
        //只要发送正文,就一定要发送响应头,所以发送响应头的内容也放在发送正文方法中进行
        /*
         * 根据文件的类型,发送对应的响应头
         * 比如发送一个css文件,那么我们发送的响应头就是mimeMapping中通过key=css,定位出对应的mime值text/css
         * 思路:
         * 1:首先根据file获取其表示的文件名(包括后缀名)
         * 2:根据文件名获取后缀名
         * 3:根据后缀从mimeMapping中获取对应的mime值,设置Content-Type的值
         * */
        //1:首先根据file获取其表示的文件名(包括后缀名) a.image.png
        String fileName = contentFile.getName();
        //2:根据文件名获取后缀名
        //substring(int i) 根据下标截取字符串(含头)
        //fileName.lastIndexOf(".") 获取fileName字符串中最后一个"."的下标
        //截取到后缀 png
        String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
        //3:根据后缀从mimeMapping中获取对应的mime值,设置Content-Type的值
        String mime = HttpContext.getMimeType(ext);
        addHeader("Content-Type", mime);
        addHeader("Content-Length", contentFile.length() + "");
    }

    /*
     * 添加一个要发送的响应头
     * */
    public void addHeader(String name, String value) {
        this.headers.put(name, value);
    }
    /*
    * 返回一个字节输出流,通过该输出流写出的字节会作为正文内容发送给浏览器
    * */
    public OutputStream getOutputStream(){
        //防止baos流对象多次创建
        if (baos == null){
            baos = new ByteArrayOutputStream();
        }
        return baos;
    }
    /*
    * 由于此处我们要对字节数出输出流进行字符串的输出,所以不能直接使用PrintWriter流了,
    * 仍旧需要自己进行流链接
    * */
    public PrintWriter getWriter(){
        return new PrintWriter(//能按行发送字符串,具有自动行刷新的功能
                new BufferedWriter(//缓冲流
                        new OutputStreamWriter(//转换流
                                getOutputStream(),//获取baos
                                StandardCharsets.UTF_8//指定编码
                        )
                ), true //自动行刷新
        );
    }
    /*
    * 设置响应头Content-Type的值
    * */
    public void setContentType(String mime){
        addHeader("Content-Type", mime);
    }
    /**
     * 要求客户端重定向到指定的路径
     */
    public void sendRedirect(String uri){
        /*
         * 重定向的响应中,状态码为302
         * 并且应当包含一个响应头Location,用来指定浏览器需要重定向的路径
         */
        //状态码
        statusCode = 302;
        //描述短语
        statusReason = "Moved Temporarily";
        //响应头
        addHeader("Location", uri);
    }
}
